namespace Brjnk.Maps.Cache
{
    using System;
    using System.Runtime.Remoting;
    using System.Runtime.Remoting.Channels;
    using System.Runtime.Remoting.Channels.Tcp;
    using System.Threading;
    using System.Windows.Media.Imaging;
    using Brjnk.Maps;
    using System.Xml;

    /// <summary>
    /// </summary>
    public class MapCache : IMapCache
    {
        private const int port = 50005;

        private string cacheDirectory;

        private Func<Exception, bool> xmlExceptionHandler;

        public MapCache(string cacheDirectory, Func<Exception, bool> xmlExceptionHandler)
        {
            this.cacheDirectory = cacheDirectory;
            Initialize(0, xmlExceptionHandler);
        }

        private bool isServer = false;

        private bool failed = false;

        private Mutex mutex;

        private MapCacheService service;

        private TcpServerChannel serverChannel;

        private TcpChannel clientCannel;

        private void Initialize(int attempt, Func<Exception, bool> xmlExceptionHandler)
        {
            try
            {
                failed = false;
                bool created;
                mutex = new System.Threading.Mutex(true, "GpxExplorerMapCache", out created);
                if (created)
                {
                    InitializeServer(xmlExceptionHandler);
                }
                else
                {
                    mutex.Dispose();
                    mutex = null;
                    InitializeClient();
                }
            }
            catch (Exception ex)
            {
                Destroy();
                if (attempt == 1)
                {
                    System.Diagnostics.Trace.Fail("MapCache error! Check if tcp port " + port + " is unused");
                    failed = true;
                    OnFailed();
                }
                else
                {
                    Initialize(attempt + 1, xmlExceptionHandler);
                }
            }
        }

        private void InitializeServer(Func<Exception, bool> xmlExceptionHandler)
        {
            isServer = true;
            System.Collections.IDictionary properties = new System.Collections.Hashtable();

            properties["port"] = port;
            properties["rejectRemoteRequests"] = true;
            serverChannel = new TcpServerChannel(properties, new BinaryServerFormatterSinkProvider());
            ChannelServices.RegisterChannel(serverChannel, true);

            try
            {
                service = new MapCacheService(cacheDirectory,false);
            }
            catch (Exception ex)
            {
                if (xmlExceptionHandler!=null && xmlExceptionHandler(ex)) service = new MapCacheService(cacheDirectory, true);
                else { failed = true; OnFailed(); }
            }
            RemotingServices.Marshal(service, "GpxExplorerCacheService.rem");
        }

        private void InitializeClient()
        {
            isServer = false;

            clientCannel = new TcpChannel();
            ChannelServices.RegisterChannel(clientCannel, true);
            MapCacheService remoteObj = (MapCacheService)Activator.GetObject(typeof(MapCacheService), "tcp://localhost:" + port + "/GpxExplorerCacheService.rem");
            service = remoteObj;
        }

        public void AddToCache(string mapName, TileId tile, System.Windows.Media.Imaging.BitmapImage image)
        {
            if ((object)image == null) return;
            SetData(tile, mapName, GetBytes(image), 0);
        }

        public Brjnk.Maps.Cache.GetCachedTileResult GetTile(string mapName, TileId tile, bool returnIfExpired)
        {
            var image = GetImage(GetData(tile, mapName, returnIfExpired, 0));
            var result = new GetCachedTileResult(mapName, tile, image);
            return result;
        }

        public void Clear()
        {
            if (failed) return;
            service.DeleteAll(true);
        }

        public void Save()
        {
            if (failed) return;
            if (isServer) service.Save();
        }

        private void SetData(TileId tileId, string mapName, byte[] data, int attempt)
        {
            if (failed) return;
            try
            {
                service.SetData(tileId.X, tileId.Y, tileId.Zoom, mapName, data);
            }
            catch (Exception ex)
            {
                Restart();
                if (attempt == 1) { ClearMutex(); failed = true; System.Diagnostics.Trace.Fail("MapCache error!"); OnFailed(); }
                SetData(tileId, mapName, data, attempt + 1);
            }
        }

        private byte[] GetData(TileId tileId, string mapName, bool expired, int attempt)
        {
            if (failed) return null;
            try
            {
                return service.GetData(tileId.X, tileId.Y, tileId.Zoom, mapName, expired);
            }
            catch (Exception ex)
            {
                Restart();
                if (attempt == 1) { Destroy(); failed = true; System.Diagnostics.Trace.Fail("MapCache error!"); OnFailed(); }
                return GetData(tileId, mapName, expired, attempt + 1);
            }
        }

        private void Restart()
        {
            Destroy();
            Initialize(0,xmlExceptionHandler);
        }

        private void Destroy()
        {
            ClearChannel();
            ClearService();
            ClearMutex();
        }

        private void ClearService()
        {
            if (isServer && service != null)
            {
                RemotingServices.Disconnect(service);
                service.Save();
            }
            service = null;
        }

        private void ClearChannel()
        {
            if (serverChannel != null)
            {
                ChannelServices.UnregisterChannel(serverChannel);
                serverChannel = null;
            }
            if (clientCannel != null)
            {
                ChannelServices.UnregisterChannel(clientCannel);
                clientCannel = null;
            }
        }

        private void ClearMutex()
        {
            if (mutex != null)
            {
                mutex.Dispose();
                mutex = null;
            }
        }

        private byte[] GetBytes(BitmapImage image)
        {
            var memory = new System.IO.MemoryStream();
            var encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(image));
            encoder.Save(memory);
            return memory.ToArray();
        }

        private BitmapImage GetImage(byte[] data)
        {
            if (data == null) return null;
            var image = new BitmapImage();
            image.BeginInit();
            image.StreamSource = new System.IO.MemoryStream(data);
            image.EndInit();
            image.Freeze();
            return image;
        }

        public event EventHandler Failed;

        protected void OnFailed()
        {
            if (Failed != null) Failed(this, EventArgs.Empty);
        }
    }
}